Skip to content

fix: widen scalar pixel_scales / shape_native at the two sites #464 missed - #485

Merged
Jammy2211 merged 1 commit into
mainfrom
feature/mask1d-shape-native-scalar-widening
Aug 23, 2026
Merged

fix: widen scalar pixel_scales / shape_native at the two sites #464 missed#485
Jammy2211 merged 1 commit into
mainfrom
feature/mask1d-shape-native-scalar-widening

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Summary

PyAutoArray#464 (8298d74e) replaced type(x) is float with validate.is_concrete_scalar
in convert_pixel_scales_1d / convert_pixel_scales_2d. Re-running that prompt's repro
found the sweep did not reach every site of the same defect. Two were still live on main,
both reproduced before being touched:

  • Mask1D.__init__ hand-rolled its own widening and never routed through
    convert_pixel_scales_1d, so it still carried the original exact-type check.
    Mask1D(mask=…, pixel_scales=1) stored the bare 1, and geometry then raised
    TypeError: 'int' object is not subscriptablefix: widen int / numpy scalar pixel_scales to a tuple #464's exact reported symptom, on a
    public constructor. Mask2D.__init__ already called convert_pixel_scales_2d, so this
    was a 1D/2D divergence rather than a design choice.
  • convert_shape_native_1d kept type(shape_native) is int, which 8298d74e listed
    as not-fixed-there. Array1D.full is its sole caller and does shape_native[0] on the
    result, so Array1D.full(shape_native=np.int32(5)) raised
    IndexError: invalid index to scalar variable.

Closes #484.

API Changes

Mask1D now widens any concrete real scalar pixel_scales (int, np.integer,
np.floating) to (float,), where previously only an exact float was widened and
anything else was stored bare. Routing it through the shared chokepoint also brings
validate.validate_pixel_scales with it, so Mask1D now rejects 0, negative and
nan pixel scales — a deliberate contract change that makes it match Mask2D, which
already rejected them. convert_shape_native_1d likewise widens any concrete integer
scalar and casts to a Python int. One new public predicate, validate.is_concrete_integer.
No symbol was removed or renamed, and no signature changed.
See full details below.

Test Plan

  • Full test_autoarray suite: 1201 passed, 0 failed. The 3 pre-existing pynufft
    failures 8298d74e baselined no longer occur, so there was nothing to baseline against.
  • Every new assertion claiming regression coverage confirmed to fail without the
    source change
    . The boundary tests (tuple returned unchanged, float/bool not
    widened, tracer passthrough) pass either way by design, mirroring those fix: widen int / numpy scalar pixel_scales to a tuple #464 shipped.
  • Real-JAX check: jax.jit compiles and runs with a traced pixel_scales, which passes
    through untouched — both functions stay jit-safe.
  • Downstream blast radius: PyAutoGalaxy and PyAutoLens only re-export Mask1D and
    construct none; neither uses Array1D.full / zeros / ones.
Mask1D(mask=…, pixel_scales=1).pixel_scales          -> (1.0,)      # was 1
…that mask's .geometry.scaled_maxima                 -> (1.5,)      # was TypeError
Array1D.full(fill_value=1.0, shape_native=np.int32(5)) builds       # was IndexError
Mask1D(…, pixel_scales=0 / -1 / nan)                 -> ValueError  # new, matches Mask2D

Test-quality fix included

#464's own widening tests asserted value only, and np.float64(1.0) == (1.0,)
NumPy-broadcasts to array([True]), which is truthy — so they passed on an unwidened NumPy
scalar and tested nothing. Four of six parametrisations were vacuous. They now assert
tuple-ness before the value, confirmed by reverting to the pre-#464 source and watching them
fail. The new tests here assert the same way for the same reason.

Not fixed here

Tuple entries are still returned unnormalised: convert_pixel_scales_2d((1, 1)) keeps its
ints and contradicts the Tuple[float, float] annotation. It alters return values on paths
that work today, so it needs its own change and its own suite read.

Full API Changes (for automation & release notes)

Added

  • autoarray.validate.is_concrete_integer(value)True for a concrete Python or NumPy
    integer scalar. The integer-only counterpart of is_concrete_scalar, for parameters
    which count pixels rather than measure them: a float returns False here where
    is_concrete_scalar accepts it. bool exclusion and tracer-safety carry over unchanged.

Changed Behaviour

  • autoarray.Mask1D.__init__pixel_scales given as any concrete real scalar (int,
    np.integer, np.floating) is now widened to (float,). Previously only an exact
    float was widened; anything else was stored bare and raised
    TypeError: 'int' object is not subscriptable on first geometry use.
  • autoarray.Mask1D.__init__ — now raises ValueError for pixel_scales of 0, a
    negative number, or nan, in both scalar and tuple form. Previously accepted silently.
    This matches Mask2D, which already validated.
  • autoarray.util.geometry.convert_shape_native_1dshape_native given as any concrete
    integer scalar (int, np.integer) is now widened to (int,) and cast to a Python
    int. Previously only an exact int was widened; an np.integer fell through and
    Array1D.full / zeros / ones raised IndexError: invalid index to scalar variable.
    A float is still deliberately not widened.

Removed

None.

Renamed

None.

Changed Signature

None.

Migration

No migration required — all three changes accept strictly more input than before, except
the Mask1D validation, which rejects pixel scales that were already unusable:

  • Before: Mask1D(mask=…, pixel_scales=0.0) constructed, then produced a division by zero
    in every pixel-to-scaled conversion.
  • After: Mask1D(mask=…, pixel_scales=0.0) raises ValueError naming the parameter and
    the received value.

Generated by the PyAutoLabs agent workflow.

…issed

PyAutoArray#464 (`8298d74e`) replaced `type(x) is float` with
`validate.is_concrete_scalar` in `convert_pixel_scales_1d` and
`convert_pixel_scales_2d`, so any concrete real scalar widens to the tuple form
both functions promise. Re-running that prompt's repro found the sweep did not
reach every site of the same defect. Two were still live on main.

`Mask1D.__init__` hand-rolled its own widening and never routed through
`convert_pixel_scales_1d`, so it still carried the original exact-type check.
`Mask1D(mask=..., pixel_scales=1)` stored the bare `1`, and the mask's geometry
then raised `TypeError: 'int' object is not subscriptable` — #464's exact
reported symptom, on a public constructor. `Mask2D.__init__` already called
`convert_pixel_scales_2d`, and `Grid1D.uniform` reaches the chokepoint too, so
this was a 1D/2D divergence rather than a design choice. It now makes the same
call `Mask2D` makes.

That also brings `validate.validate_pixel_scales` to `Mask1D`, which is a
deliberate contract change: `Mask1D` now rejects `0`, negative and `nan` pixel
scales exactly as `Mask2D` already did. No test constructed one that way and all
12 library call sites pass real scales, so nothing needed adjusting to suit it.

`convert_shape_native_1d` kept `type(shape_native) is int`, which `8298d74e`
listed as not-fixed-there. `Array1D.full` is its sole caller and does
`shape_native[0]` on the result, so `Array1D.full(shape_native=np.int32(5))`
raised `IndexError: invalid index to scalar variable`. It now tests
`validate.is_concrete_integer` and casts to a Python `int`.

`is_concrete_integer` is new, beside `is_concrete_scalar`: `shape_native` counts
pixels rather than measuring them, so `is_concrete_scalar` is the wrong predicate
there — it would silently widen a `float`, which is a mistake worth surfacing.
`bool` exclusion and tracer-safety carry over unchanged, so both functions stay
safe inside a `jax.jit`; verified by compiling and running one.

Also tightened #464's own widening tests. `np.float64(1.0) == (1.0,)`
NumPy-broadcasts to `array([True])`, which is truthy, so their value-only
assertions passed on an unwidened NumPy scalar and tested nothing. Asserting
tuple-ness before the value makes them fail on the pre-#464 source (confirmed by
reverting it), where four of the six parametrisations previously passed
vacuously. The new tests here assert the same way for the same reason.

Not fixed here, needing its own change: tuple entries are still returned
unnormalised, so `convert_pixel_scales_2d((1, 1))` keeps its ints and
contradicts the `Tuple[float, float]` annotation. That alters return values on
paths which work today.

Validation: 1201 passed / 0 failed on the full test_autoarray suite. The 3
pre-existing pynufft failures `8298d74e` baselined no longer occur, so there was
nothing to baseline against. Every new assertion that claims regression coverage
was confirmed to fail without the source change; the boundary tests (tuple
unchanged, float/bool not widened, tracer passthrough) pass either way by design,
mirroring the ones #464 shipped.

Downstream blast radius is nil: PyAutoGalaxy and PyAutoLens only re-export
`Mask1D` and construct none, and neither uses `Array1D.full`/`zeros`/`ones`.

Closes #484

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fj1HoQa4hZPmbyyBYNJX62
@Jammy2211 Jammy2211 added the pending-release PR queued for the next release build label Aug 23, 2026
@Jammy2211
Jammy2211 merged commit 9e47505 into main Aug 23, 2026
3 checks passed
@Jammy2211
Jammy2211 deleted the feature/mask1d-shape-native-scalar-widening branch August 23, 2026 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pending-release PR queued for the next release build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: widen scalar pixel_scales / shape_native at the two sites #464 missed

1 participant